import logging
import math
from recordclass import recordclass

from coin.base.timestamp import get_timestamp

LastFill = recordclass('FillInternal', [
    'timestamp_last_fill',
    'qty_last_fill',
    'price_last_fill',
])

FillInternal = recordclass('FillInternal',
                           [
                               'timestamp_created',
                               'timestamp_last_fill',
                               'qty_orig',
                               'qty_accum_fill',
                               'qty_last_fill',
                               'price_orig',
                               'price_avg_fill',
                               'price_last_fill',
                           ])


def is_close_to_zero(value, abs_tol=1e-10):
  return math.isclose(value, 0., abs_tol=abs_tol)


class FillManager(object):
  def __init__(self,
               *,
               timestamp_created=None,
               timestamp_last_fill=None,
               qty_orig=None,
               qty_accum_fill=0.0,
               qty_last_fill=None,
               price_orig=None,
               price_avg_fill=None,
               price_last_fill=None):
    self._value = FillInternal(timestamp_created=timestamp_created or get_timestamp(),
                               timestamp_last_fill=timestamp_last_fill,
                               qty_orig=qty_orig,
                               qty_accum_fill=qty_accum_fill,
                               qty_last_fill=qty_last_fill,
                               price_orig=price_orig,
                               price_avg_fill=price_avg_fill,
                               price_last_fill=price_last_fill)
    self._logger = logging.getLogger(__name__)

  def __str__(self):
    return str(self._value)

  def __repr__(self):
    return repr(self._value)

  @property
  def value(self):
    return self._value

  # For some exchanges, when order is fully filled,
  # qty_accum_fill is less than qty_orig due to trading fee.
  # Override this property in the subclass if necessary.
  @property
  def fully_filled(self):
    return math.isclose(self._value.qty_accum_fill,
                        self._value.qty_orig,
                        rel_tol=1e-3,
                        abs_tol=1e-8)

  def update_by_last_fill(self, price_last_fill, qty_last_fill):
    timestamp = get_timestamp()
    self._value.price_last_fill = price_last_fill
    self._value.qty_last_fill = qty_last_fill

    prev_price_avg_fill = self._value.price_avg_fill or 0.0
    self._value.price_avg_fill = (prev_price_avg_fill * self._value.qty_accum_fill + price_last_fill
                                  * qty_last_fill) / (self._value.qty_accum_fill + qty_last_fill)
    self._value.qty_accum_fill += qty_last_fill
    if self._value.qty_orig and self._value.qty_accum_fill > self._value.qty_orig + 1e-8:
      self._logger.error(
          'Insane qty_accum_fill (update_by_last_fill) qty_orig=%s, qty_accum_fill=%s',
          self._value.qty_orig,
          self._value.qty_accum_fill)
      return None
    self._value.timestamp_last_fill = timestamp
    return LastFill(timestamp_last_fill=timestamp,
                    price_last_fill=price_last_fill,
                    qty_last_fill=qty_last_fill)

  def update_by_accum_fill(self, price_avg_fill, qty_accum_fill):
    # First fill message.
    if is_close_to_zero(self._value.qty_accum_fill):
      assert self._value.price_avg_fill is None or is_close_to_zero(self._value.price_avg_fill)

      if is_close_to_zero(qty_accum_fill):
        return None

      assert price_avg_fill > 0.0
      self._value.price_avg_fill = price_avg_fill
      self._value.qty_accum_fill = qty_accum_fill
      self._value.price_last_fill = price_avg_fill
      self._value.qty_last_fill = qty_accum_fill
      return LastFill(timestamp_last_fill=get_timestamp(),
                      price_last_fill=price_avg_fill,
                      qty_last_fill=qty_accum_fill)

    # if qty_accum_fill is same, duplicate update.
    # if qty_accum_fill decreases, old update.
    if math.isclose(self._value.qty_accum_fill, qty_accum_fill) or \
       self._value.qty_accum_fill > qty_accum_fill:
      # No change to fillinfo
      return None

    if self._value.qty_orig and (qty_accum_fill
                                 - self._value.qty_orig) / self._value.qty_orig > 1e-3:
      self._logger.error(
          'Insane qty_accum_fill (update_by_accum_fill) qty_orig=%s, qty_accum_fill=%s',
          self._value.qty_orig,
          qty_accum_fill)
      return None

    # Normal case.
    assert qty_accum_fill > self._value.qty_accum_fill
    qty_last_fill = qty_accum_fill - self._value.qty_accum_fill
    price_last_fill = (price_avg_fill * qty_accum_fill
                       - self._value.price_avg_fill * self._value.qty_accum_fill) / qty_last_fill
    self._value.price_avg_fill = price_avg_fill
    self._value.qty_accum_fill = qty_accum_fill
    self._value.price_last_fill = price_last_fill
    self._value.qty_last_fill = qty_last_fill
    return LastFill(timestamp_last_fill=get_timestamp(),
                    price_last_fill=price_last_fill,
                    qty_last_fill=qty_last_fill)

  def log_last_fill(self, order, logger):
    logger.info('%s FILL (%s, %s): %s %f,%f/%f @ %f' % (order.product.exchange,
                                                        order.product,
                                                        order.order_id,
                                                        order.order_side.name,
                                                        self.value.qty_last_fill or 0.,
                                                        self.value.qty_accum_fill or 0.,
                                                        self.value.qty_orig or 0.,
                                                        self.value.price_last_fill or 0.))
